跳到主要内容

Java 并发编程-通信

概述

一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当需要多个线程之间相互协作的时候,就需要 Java线程的通信

synchronized 锁的基本使用

多个线程操作同一个资源的情况下,线程不安全,数据混乱,所以引入了 synchronized 关键字,当一个线程获得对象的排它锁,独占资源,其他线程必须等待使用完成后释放锁即可,synchronized 可以附在块上也可以附在方法上

例如这里让线程按顺序执行,定义一个对象锁住代码块

public class ObjectLock {
private static final Object lock = new Object();

static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread A " + i);
}
}
}
}

static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread B " + i);
}
}
}
}

public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(10);
new Thread(new ThreadB()).start();
}
}

这样就会在 A 执行完 run 里面的方法后才执行 B 方法

这个没啥好说,具体看基本概念那一篇

synchronized 方法导致的死锁

在同一个类里多处使用 synchronized 方法会导致死锁的发生,看如下代码

public class Temp {
public static void main(String[] args) throws InterruptedException {
SyncDemo syncDemo = new SyncDemo();

Thread threadA = new Thread(() -> {
syncDemo.addTask("threadA 插入了一条消息");
});

Thread threadB = new Thread(() -> {
System.out.println("threadB 获取到:" + syncDemo.getTask());
});

threadB.start();
Thread.sleep(10); // 确保是 threadB 先执行
threadA.start();
}


static class SyncDemo {
Queue<String> queue = new LinkedList<>();

public synchronized void addTask(String s) {
this.queue.add(s);
}

public synchronized String getTask() {
while (queue.isEmpty()) {
Thread.onSpinWait();
}
return queue.remove();
}
}
}

乍一看没有什么问题,但实际上 while() 循环永远不会退出,因为这里的 getTask()addTask() 方法 都使用的这个实例的 this 锁

所以线程调用了其中一个方法就会导致另一个方法无法执行,这里的 B 线程先调用了 getTask() 方法,它里面内含了一个 while 循环导致这个方法一直都不会被结束,从而导致这个锁永远不会被释放

这里主要问题就是获取锁的顺序不对,让 getTask 先取得了锁,导致它一直不释放锁,从而令这个类的其它方法被锁住,从而导致死锁的发生

这一节主要是用来引出下面的下面的信号量机制

wait / notify 机制

参考资料 5 Java线程间的通信 参考资料 廖雪峰的官方网站 使用wait和notify

这里还是拿上面的代码做例子,上面的代码实际想要的执行效果是:

  • 线程A 可以调用 addTask() 不断往队列中添加任务;
  • 线程B 可以调用 getTask() 从队列中获取任务。如果队列为空,则 getTask() 应该等待,直到队列中至少有一个任务时再返回。

因此,多线程协调运行的原则就是:

  • 当条件不满足时,线程进入等待状态;
  • 当条件满足时,线程被唤醒,继续执行任务。

这种时候就可以采用 Java 多线程的 wait / notify 机制,它是基于 Object 类的 wait() 方法和 notify()notifyAll() 方法来实现的。

首先说明一下它们的作用

wait():会释放自己获取的锁,直到被唤醒后才重新获取这个锁 notify():随机叫醒一个正在等待的线程 notifyAll():会叫醒所有正在等待的线程

上面的代码改成如下这样

public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll(); // 唤醒下面的 wait()
}

public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
// 释放 this 锁
this.wait();
// 重新获取 this 锁
}
return queue.remove();
}

上面这个代码因为 while 每次只需要执行一次判断,所以在它判断完后加个 this.wait() 表示它已经判断完了,等待下次唤醒再次判断,而这个 this.wait() 还可以把锁释放出来给其它需要锁的方法使用

不过注意:这里的例子中 wait() 方法必须在 当前获取的锁对象上调用,因为这里获取的是 this 锁,因此调用 this.wait()。其次,必须在 synchronized 块中才能调用 wait() 方法,因为 wait() 方法调用时,会释放线程获得的锁,wait() 方法返回后,线程又会重新试图获得锁。

调用 wait() 方法后,线程进入等待状态,wait() 方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后,wait() 方法才会返回,然后,继续执行下一条语句。

wait / notify 生产消费案例

当需要面临生产者生产一个,消费者消费一个的场景时也可以使用 wait / notify 来解决

public class Temp {
/**
* 创建一个 Storage 类,用来 “生产”、“消费” 数据
*/
static class Storage {
int source = 0;

// 生产者
public synchronized void produce() throws InterruptedException {
// 检查库存,如果还有库存没有消费就等待
if (source > 0) this.wait();
source++;
System.out.println("生产了一个资源,当前库存为:" + source);
// 唤醒全部线程
this.notifyAll();
}

// 消费者
public synchronized void consumer() throws InterruptedException {
// 如果无库存了则等待
if (source <= 0) this.wait();
source--;
System.out.println("消费了一个资源,当前库存为:" + source);
this.notifyAll();
}
}

public static void main(String[] args) throws InterruptedException {
CountDownLatch cd = new CountDownLatch(2);
Storage storage = new Storage();

// 生产者线程
new Thread(() -> {
// 生产 1000 次
for (int i = 0; i < 1000; i++) {
try {
storage.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
cd.countDown();
}).start();

// 消费者线程
new Thread(() -> {
// 消费 1000 次
for (int i = 0; i < 1000; i++) {
try {
storage.consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
cd.countDown();
}).start();

cd.await();
System.out.println("当前库存为:" + storage.source);
}
}

注意这个代码实际是错的,具体看下面的解析

生产消费-虚假唤醒的问题

上面一个生产线程一个消费线程,它们的执行是交替执行的,但是如果改成多个生产线程,和多个消费线程呢?结果就是会导致 虚假唤醒 的问题

虚假唤醒:当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功。比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁

解决的办法就是把 if 改成 while

if (source > 0) this.wait();

// 改成

while (source > 0) this.wait();

if 为什么会出现虚假唤醒?

因为 if 只会执行一次,执行完会接着向下执行 if() 外边的,而 while 不会,直到条件满足才会向下执行 while() 外边的

虚假唤醒之抢票问题

先来说问题代码

public class Temp {
int count = 1000;


public static void main(String[] args) {
Temp temp = new Temp();

new Thread(temp::minus, "线程一").start();
new Thread(temp::minus, "线程二").start();
new Thread(temp::minus, "线程三").start();
new Thread(temp::minus, "线程四").start();
}

private void minus() {
while (count > 0) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "卖出一张票,当前票为:" + --count);
}
}
}
}

会发现到最后几张票是负数的,这是因为在 count > 0 这里大家都通过了,但是只能有一个线程获取锁,这时其它的线程就是假唤醒

线程一卖出一张票,当前票为:3
线程一卖出一张票,当前票为:2
线程一卖出一张票,当前票为:1
线程一卖出一张票,当前票为:0
线程二卖出一张票,当前票为:-1
线程四卖出一张票,当前票为:-2
线程三卖出一张票,当前票为:-3

方法一:使用 wait、notify 机制(老实说这种方法挺麻烦的,但是可以用于理解在同一个方法里使用 wait、notify 的场景)

将其改成如下,首先判断一下它的逻辑:第一个线程进入这个方法后,它拿到了锁 --count 后,就执行 this.wait() 释放了锁,等待下一个线程进来取得锁将其唤醒,但是因为这个新进来的线程取得了锁,所以第一个线程也需要挂起,如此循环。直到第一个线程被后面的线程唤醒执行才执行 count > 0 判断。

实际上这是一种把独占方法的 while 转成队列

public class Temp {
int count = 1000;

public static void main(String[] args) {
Temp temp = new Temp();
new Thread(temp::minus, "线程一").start();
new Thread(temp::minus, "线程二").start();
new Thread(temp::minus, "线程三").start();
new Thread(temp::minus, "线程四").start();
}

private synchronized void minus() {
try {
while (count > 0) {
this.notifyAll();
System.out.println(Thread.currentThread().getName() + "卖出一张票,当前票为:" + --count);
this.wait(); // 这个 wait 使用是用来等待 while 的那个判断
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.notifyAll(); // 最后一定要唤醒所有线程
}
}
}

方法二:直接使用 if 判断

不整那么多花里胡哨的,上面之所以会出现负数就是因为那是在拿到锁之前判断的,如果拿到锁再判断就没有那么多事了

public class Temp {
int count = 1000;


public static void main(String[] args) {
Temp temp = new Temp();

new Thread(temp::minus, "线程一").start();
new Thread(temp::minus, "线程二").start();
new Thread(temp::minus, "线程三").start();
new Thread(temp::minus, "线程四").start();
}

private void minus() {
while (true) {
synchronized (this) {
if (count <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,当前票为:" + --count);
}
}
}
}

Condition 监视器(wait、notifyAll)

前面的生产者消费者问题都是使用的 synchronized 和 notifyAll()wait() 实现的,那使用 Lock 如何实现呢?关键就在于如何找到代替 wait、notifyAll 的工具?

这时就需要使用 Condition 工具类来实现 wait 和 notify 的功能

public class Temp {
/**
* 创建一个 Storage 类,用来 “生产”、“消费” 数据
*/
static class Storage {
int source = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

// 生产者
public void produce() throws InterruptedException {
lock.lock();
// 检查库存,如果还有库存没有消费就等待
try {
while (source > 0) condition.await();
source++;
System.out.println("生产了一个资源,当前库存为:" + source);
} finally {
// 唤醒全部线程
condition.signalAll();
lock.unlock();
}
}

// 消费者
public void consumer() throws InterruptedException {
lock.lock();
try {
// 如果无库存了则等待
while (source <= 0) condition.await();
source--;
System.out.println("消费了一个资源,当前库存为:" + source);
} finally {
condition.signalAll();
lock.unlock();
}
}
}

public static void main(String[] args) throws InterruptedException {
CountDownLatch cd = new CountDownLatch(6);
Storage storage = new Storage();

// 创建 3个生产者线程
for (int j = 0; j < 3; j++) {
new Thread(() -> {
// 生产 1000 次
for (int i = 0; i < 1000; i++) {
try {
storage.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
cd.countDown();
}).start();
}

// 创建 3个消费者线程
for (int j = 0; j < 3; j++) {
new Thread(() -> {
// 消费 1000 次
for (int i = 0; i < 1000; i++) {
try {
storage.consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
cd.countDown();
}).start();
}

cd.await();
System.out.println("当前库存为:" + storage.source);
}
}

Condition 精准唤醒其它线程

让线程按步骤执行

public class Temp {
/**
* 创建一个 Storage 类,用来 “生产”、“消费” 数据
*/
static class Coder {
// 记录当前步骤
int state = 1;
Lock lock = new ReentrantLock();
// 获取监视器
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();

public void step1() {
lock.lock();
try {
while (state != 1) {
condition1.await();
}
System.out.println("1>编写源代码");
// 修改步骤
state = 2;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 唤醒第二个监视器
condition2.signalAll();
lock.unlock();
}


}

public void step2() {
lock.lock();
try {
while (state != 2) {
condition2.await();
}
System.out.println("2>将源代码编译为字节码文件");
// 修改步骤
state = 3;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 唤醒第三个监视器
condition3.signalAll();
lock.unlock();
}
}

public void step3() {
lock.lock();
try {
while (state != 3) {
condition3.await();
}
System.out.println("3>执行字节码文件");
// 修改步骤
state = 1;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 唤醒第一个监视器
condition1.signalAll();
lock.unlock();
}
}

}

public static void main(String[] args) {
Coder coder = new Coder();

// 编写源代码
new Thread(()-> {
for (int i = 0; i < 10; i++) {
coder.step1();
}
}).start();

// 将源代码编译为字节码文件
new Thread(()-> {
for (int i = 0; i < 10; i++) {
coder.step2();
}
}).start();

// 执行字节码文件
new Thread(()-> {
for (int i = 0; i < 10; i++) {
coder.step3();
}
}).start();
}
}

例如如上面的代码,不管循环多少次,都是按照下面这个顺序执行

1>编写源代码
2>将源代码编译为字节码文件
3>执行字节码文件
1>编写源代码
2>将源代码编译为字节码文件
3>执行字节码文件
...

Semaphore 信号量:并发数量控制

参考资料 17、详解java线程同步工具Semaphore的使用

信号量(Semaphore)主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

Semaphore 用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。 一个线程获取许可证就调用 acquire 方法,用完了释放资源就调用 release 方法。

不过这样的解释实在有点抽象,现在用我自己的话来解释一下:

相信在学生时代都去餐厅打过饭,假如有3个窗口可以打饭,同一时刻也只能有3名同学打饭。第四个人来了之后就必须在外面等着,只要有打饭的同学好了,就可以去相应的窗口了。

比如说这张图,就全是了Semaphore的基本使用。认识一个知识点的最好方式就是直接去使用,我们干脆直接上代码来看看如何使用。

它常用的方法就下面这两个

// 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
acquire() // acquire(获取)当一个线程调用 acquire 操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。


// 释放一个许可,将其返回给信号量。
release() // release(释放)实际上会将信号量的值加 1,然后唤醒等待的线程。

Semaphore 使用案例:3 个停车位,6 辆车去抢,走一辆,抢一个停车位。

public class Temp {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);

// 创建 6个线程
for (int i = 0; i < 6; i++) {

// 在线程里面 “抢车位”,每个线程都能抢到车位,但是顺序不同,后面的线程得等前面的线程离开后才能取得车位
new Thread(()-> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"号抢到车位");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"号离开");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放一个许可
semaphore.release();
}
}, "线程:\t " + i).start();
}
}
}

如上的例子所示,最多只有 3个线程同时取得 semaphore 其它线程得等其中一个线程执行 release() 方法才有机会去执行

LockSupport 线程的阻塞和唤醒 🔥

LockSupport 是构建同步组件的基础工具,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。

这个工具类的所有方法都是静态的,底层采用 UNSAFE 直接操作的内存,可以实现线程的阻塞和唤醒

public static void park() {
UNSAFE.park(false, 0L);
}

所以我们大概知道这么多就可以了,当然有一点非常重要,那就是 LockSupport 在进行线程阻塞和唤醒的时候是不需要获取锁的

作用:

  • park:阻塞一个线程
  • unpark:唤醒一个线程

为什么说是构建同步组件的基础工具呢,是因为 AQS 中的阻塞和唤醒就是基于 LockSupport 做的

而 ReentrantLock 中的 Sync 又是继承了 AQS 来完成的锁,所以说他是构建同步组件的基础工具

如何使用呢?

具体看下面 多线程顺序打印 “ABCABC” 那一节

使用 volatile 实现信号量

让线程A 输出 0,然后线程B 输出 1,再然后线程A 输出 2…以此类推

public class Temp {
private static volatile int signal = 0;

static class ThreadA implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 0) {
System.out.println("threadA: " + signal);
synchronized (this) {
signal++;
}
}
}
}
}

static class ThreadB implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 1) {
System.out.println("threadB: " + signal);
synchronized (this) {
signal = signal + 1;
}
}
}
}
}

public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
TimeUnit.SECONDS.sleep(1);
new Thread(new ThreadB()).start();
}
}

使用了一个 volatile 变量 signal 来实现了 “信号量” 的模型。这里需要注意的是,volatile 变量需要进行原子操作。signal++ 并不是一个原子操作,所以需要使用 synchronized 给它“上锁”。

管道流:多线程传输数据

参考资料 Java IO7:管道流、对象流 参考资料 Java Pipe(管道)

这个在学习 IO那块时已经学习过了

Java 里的管道输入流 PipedInputStream 与管道输出流 PipedOutputStream 实现了类似管道的功能,用于将数据从一个线程传输到另一个线程。两个线程之间的同步由阻塞读和写来处理。

管道 I/O 基于生产者 - 消费者模式,其中生产者产生数据,而消费者消费数据。在管道 I/O 中,创建两个流代表管道的两端。 PipedOutputStream 对象表示流的一端,PipedInputStream 对象则表示流的另一端。使用两个对象的 connect() 方法连接两端。

还可以通过在创建另一个对象时将一个对象传递给构造函数来连接它们。以下代码显示了创建和连接管道两端的两种方法:

第一种方法创建管道输入和输出流并连接它们。 它使用 connect() 方法连接两个流。

PipedInputStream pis  = new PipedInputStream(); 
PipedOutputStream pos = new PipedOutputStream();
pis.connect(pos); /* Connect the two ends */

第二种方法创建管道输入和输出流并连接它们。 它通过将输入管道流传递到输出流构造器来连接两个流。

PipedInputStream pis  = new PipedInputStream(); 
PipedOutputStream pos = new PipedOutputStream(pis);

管道流的工作如下图所示:

801753-20151019214245427-871546194.png

下面是具体使用例子:

既然管道流的作用是用于线程间的通信,那么势必有发送线程和接收线程,两个线程通过管道流交互数据。首先写一个发送数据的线程:

public class Sender implements Runnable {
private PipedOutputStream out = new PipedOutputStream();

public PipedOutputStream getOutputStream() {
return out;
}

public void run() {
String str = "你好,这里是管道流";
try {
out.write(str.getBytes()); // 向管道流中写入数据(发送)
out.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}

既然有一个发送数据的线程了,接下来来一个接收数据的线程:

public class Receiver implements Runnable {
private PipedInputStream in = new PipedInputStream();

public PipedInputStream getInputStream() {
return in;
}

public void run() {
String s = null;
byte b0[] = new byte[1024];

try {
int length = in.read(b0);
if (-1 != length)
{
s = new String(b0, 0 , length);
System.out.println("收到了以下信息:" + s);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

写一个 main 线程,利用管道输出流的 connect 方法连接管道输出流和管道输入流:

public static void main(String[] args) {
try {
Sender sender = new Sender();
Receiver receiver = new Receiver();

Thread senderThread = new Thread(sender);
Thread receiverThread = new Thread(receiver);

PipedOutputStream out = sender.getOutputStream(); // 写入
PipedInputStream in = receiver.getInputStream(); // 读出

out.connect(in);// 将输出发送到输入

senderThread.start();
receiverThread.start();
}
catch (IOException e) {
e.printStackTrace();
}
}

默认 PipedInputStream 运用的是一个 1024 字节固定大小的循环缓冲区,写入 PipedOutputStream 的数据实际上保存到了对应的 PipedInputStream 的内部缓冲区。

PipedInputStream 执行读操作时,读取的数据实际上来自这个内部缓冲区。如果对应的 PipedInputStream 输入缓冲区已满,任何企图写入 PipedOutputStream 的线程都将被阻塞。而且这个写操作线程将一直阻塞,直至出现读取 PipedInputStream 的操作从缓冲区删除数据。

但是可以在实例化时指定这个缓冲区大小,以下代码创建缓冲区容量为 2048 字节的管道输入流。

PipedOutputStream pos  = new PipedOutputStream(); 
PipedInputStream pis = new PipedInputStream(pos, 2048);

多线程顺序打印 “ABCABC” 🔥

转载自 华为和阿里都考过的多线程编程题,你会吗?多线程交替打印 ABC的多种实现方法

经典并发编程问题,这里就使用上面所学到的知识来解决这个问题

编写一个程序,开启三个线程,这三个线程的 ID 分别是 A、B 和 C,每个线程把自己的 ID 在屏幕上打印 10 遍,要求输出结果必须按 ABC 的顺序显示,如 ABCABCABC... 依次递推

这是一道经典的多线程编程面试题,首先吐槽一下,这道题的需求很是奇葩,先开启多线程,然后再串行打印 ABC,这不是吃饱了撑的吗?不过既然是道面试题,就不管这些了,其目的在于考察你的多线程编程基础。就这道题,你要是写不出个三四种解法,你都不好意思说你学过多线程。哈哈开玩笑,下面就为你介绍一下本题的几种解法。

下面有几种解决的方式:

使用 LockSupport

LockSupport 是 java.util.concurrent.locks 包下的工具类,它的静态方法 unpark()park() 可以分别实现阻塞当前线程和唤醒指定线程的效果,所以用它解决这样的问题简直是小菜一碟,代码如下:

public class PrintABC {

static Thread threadA, threadB, threadC;

public static void main(String[] args) {
threadA = new Thread(() -> {
for (int i = 0; i < 10; i++) {
// 打印当前线程名称
System.out.print(Thread.currentThread().getName());
// 唤醒下一个线程
LockSupport.unpark(threadB);
// 当前线程阻塞
LockSupport.park();
}
}, "A");

threadB = new Thread(() -> {
for (int i = 0; i < 10; i++) {
// 先阻塞等待被唤醒
LockSupport.park();
System.out.print(Thread.currentThread().getName());
// 唤醒下一个线程
LockSupport.unpark(threadC);
}
}, "B");
threadC = new Thread(() -> {
for (int i = 0; i < 10; i++) {
// 先阻塞等待被唤醒
LockSupport.park();
System.out.print(Thread.currentThread().getName());
// 唤醒下一个线程
LockSupport.unpark(threadA);
}
}, "C");
threadA.start();
threadB.start();
threadC.start();
}
}

使用 synchronized 加 wait、notifyAll

这种方法就是直接使用 Java 的 synchronized 关键字,配合 Object 的 wait()notifyAll() 方法实现线程交替打印的效果,不过这种写法的复杂度和代码量都偏大。

由于 notify()notifyAll() 方法都不能唤醒指定的线程,所以需要三个布尔变量对线程执行顺序进行控制。另外要注意的就是,for 循环中的 i++ 需要在线程打印之后执行,否则每次被唤醒后,不管是不是轮到当前线程打印都会执行 i++,这显然不是我们想要的。代码如下 (一般 B、C 线程和 A 线程的执行逻辑类似,只在 A 线程代码中进行详细注释说明):

public class PrintABC {
// 使用布尔变量对打印顺序进行控制,true表示轮到当前线程打印
private static boolean startA = true;
private static boolean startB = false;
private static boolean startC = false;

public static void main(String[] args) {
// 作为锁对象
final Object o = new Object();
// A线程
new Thread(() -> {
synchronized (o) {
for (int i = 0; i < 10; ) {
if (startA) {
// 代表轮到当前线程打印
System.out.print(Thread.currentThread().getName());
// 下一个轮到B打印,所以把startB置为true,其它为false
startA = false;
startB = true;
startC = false;
// 唤醒其他线程
o.notifyAll();
// 在这里对i进行增加操作
i++;
} else {
// 说明没有轮到当前线程打印,继续wait
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "A").start();
// B线程
new Thread(() -> {
synchronized (o) {
for (int i = 0; i < 10; ) {
if (startB) {
System.out.print(Thread.currentThread().getName());
startA = false;
startB = false;
startC = true;
o.notifyAll();
i++;
} else {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "B").start();
// C线程
new Thread(() -> {
synchronized (o) {
for (int i = 0; i < 10; ) {
if (startC) {
System.out.print(Thread.currentThread().getName());
startA = true;
startB = false;
startC = false;
o.notifyAll();
i++;
} else {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "C").start();
}
}

使用 Lock 搭配 Condition 监视器实现

使用 synchronized 锁机制的写法着实有些复杂,何不试试 ReentrantLock?这是 java.util.concurrent.locks 包下的锁实现类,它拥有更灵活的 API,能够对多线程执行流程实现更精细的控制,特别是在搭配 Condition 使用的情况下,可以随心所欲地控制多个线程的执行顺序,来看看这个组合在本题中的使用吧,代码如下:

其实和上面的方法差不多

public class PrintABC {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 使用ReentrantLock的newCondition()方法创建三个Condition
// 分别对应A、B、C三个线程
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();

// A线程
new Thread(() -> {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.print(Thread.currentThread().getName());
// 叫醒B线程
conditionB.signal();
// 本线程阻塞
conditionA.await();
}
// 这里有个坑,要记得在循环之后调用signal(),否则线程可能会一直处于
// wait状态,导致程序无法结束
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 在finally代码块调用unlock方法
lock.unlock();
}
}, "A").start();
// B线程
new Thread(() -> {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.print(Thread.currentThread().getName());
conditionC.signal();
conditionB.await();
}
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
// C线程
new Thread(() -> {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.print(Thread.currentThread().getName());
conditionA.signal();
conditionC.await();
}
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "C").start();
}
}

使用 Semaphore 实现

semaphore中文意思是信号量,原本是操作系统中的概念,JUC下也有个 Semaphore 的类,可用于控制并发线程的数量。Semaphore 的构造方法有个 int 类型的 permits 参数,如下:

public Semaphore(int permits) {...}

其中 permits 指的是该 Semaphore 对象可分配的许可数,一个线程中的 Semaphore 对象调用 acquire() 方法可以让线程获取许可继续运行,同时该对象的许可数减一,如果当前没有可用许可,线程会阻塞。该 Semaphore 对象调用 release() 方法可以释放许可,同时其许可数加一。

其实实现的方法和上面一样

public class PrintABC {

public static void main(String[] args) {
// 初始化许可数为1,A线程可以先执行
Semaphore semaphoreA = new Semaphore(1);
// 初始化许可数为0,B线程阻塞
Semaphore semaphoreB = new Semaphore(0);
// 初始化许可数为0,C线程阻塞
Semaphore semaphoreC = new Semaphore(0);

new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
// A线程获得许可,同时semaphoreA的许可数减为0,进入下一次循环时
// A线程会阻塞,知道其他线程执行semaphoreA.release();
semaphoreA.acquire();
// 打印当前线程名称
System.out.print(Thread.currentThread().getName());
// semaphoreB许可数加1
semaphoreB.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
semaphoreB.acquire();
System.out.print(Thread.currentThread().getName());
semaphoreC.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
semaphoreC.acquire();
System.out.print(Thread.currentThread().getName());
semaphoreA.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}